package org.infodavid.common.persistence;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.PersistenceException;

import junit.framework.Assert;

import org.infodavid.common.model.AbstractBeanTest;
import org.infodavid.common.model.AbstractPersistenceTest;
import org.infodavid.common.persistence.exceptions.ConstraintViolatedException;
import org.infodavid.common.util.TimeUtil;
import org.joda.time.DateTime;
import org.junit.Test;

/**
 * The Class AbstractDataServiceTest.
 * @param <L> the generic type
 * @param <T> the generic type
 * @param <K> the key type
 */
@SuppressWarnings("rawtypes")
public abstract class AbstractDataServiceTest<L extends IDataObject<K>,T extends L,K extends Serializable>
    extends AbstractPersistenceTest {
  /**
   * The Class ConcurrentThread.
   */
  public class ConcurrentThread extends Thread {

    /** The key. */
    private final K key;

    /**
     * Instantiates a new concurrent thread.
     * @param arg0 the arg0
     */
    public ConcurrentThread(final K arg0) {
      super();

      key = arg0;
    }

    /**
     * See super class or interface. (non-Javadoc)
     * @see java.lang.Thread#run()
     */
    @Override
    public void run() {
      PersistenceSession session2 = null;
      ITransaction transaction2;

      try {
        session2 = getManager().open();
        transaction2 = session2.getTransaction();

        transaction2.begin();

        final T item = getService().findByKey(session2, key);

        modify(item);

        getService().update(session2, item);
        transaction2.commit();
      }
      catch (final Exception e) {
        e.printStackTrace();
      }
      finally {
        if (session2 != null) {
          try {
            if (session2.isOpen()) {
              session2.close();
            }
          }
          catch (final Exception e2) {
            e2.printStackTrace();
          }
        }
      }
    }
  }

  /** The service. */
  private final IDataService<T,K> service;

  /** The bean test. */
  private final AbstractBeanTest<T> beanTest;

  /**
   * The Constructor.
   * @param arg0 the arg0
   * @param arg1 the arg1
   */
  public AbstractDataServiceTest(final AbstractBeanTest<T> arg0, final IDataService<T,K> arg1) {
    super(arg0);

    beanTest = arg0;
    service = arg1;
  }

  /**
   * Creates the.
   * @param count the count
   * @return the collection
   * @throws Exception the exception
   */
  public List<T> create(final int count) throws Exception {
    return beanTest.create(count);
  }

  /**
   * Returns the number of rows.
   * @return the number of rows
   * @throws PersistenceException the persistence exception
   */
  protected long count() throws PersistenceException {
    final Object obj =
        getSession().executeQuery(beanTest.getEntityClass(),
            beanTest.getEntityName() + ".selectAll.count", null);

    return obj instanceof Number ? ((Number)obj).longValue() : 0;
  }

  /**
   * Test persist entity.
   * @throws Exception the exception
   */
  @SuppressWarnings("null")
  protected void doInsertDuplicatedEntity() throws Exception {
    final PersistenceSession session = getSession();
    final T item1 = getTestData().get(0);
    final T item2 = service.clone(item1, true, true);
    Exception exception = null;

    service.insert(session, item1);

    try {
      service.insert(session, item2);
    }
    catch (final Exception e) {
      exception = e;
      e.printStackTrace();
    }

    Assert.assertNotNull("No exception thrown", exception);
    Assert.assertEquals("Wrong exception thrown", ConstraintViolatedException.class,
        exception.getClass());
  }

  /**
   * Gets the service.
   * @return the service
   */
  public final IDataService<T,K> getService() {
    return service;
  }

  /**
   * Gets the test data.
   * @return the test data
   */
  protected List<T> getTestData() {
    return beanTest.getTestData();
  }

  /**
   * Modifies the data to check concurrent access management.
   * @param item the object to modify
   * @throws Exception the exception
   */
  public void modify(final T item) throws Exception {}

  /**
   * Test clear.
   * @throws Exception the exception
   */
  @Test
  public void testClear() throws Exception {
    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, count());

    service.deleteAll(getSession());

    Assert.assertEquals("Not all data has been inserted", 0, count());
  }

  /**
   * Test count.
   * @throws Exception the exception
   */
  @Test
  public void testCount() throws Exception {
    final IQueryParameter param = QueryParameterFactory.getSingleton().borrow();

    try {
      Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, count());
      Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT,
          service.count(getSession()));
    }
    finally {
      QueryParameterFactory.getSingleton().release(param);
    }
  }

  /**
   * Test create.
   * @throws Exception the exception
   */
  @Test
  public void testCreate() throws Exception {
    final Object instance = service.create();

    Assert.assertNotNull("Object not built", instance);
    Assert.assertTrue("Wrong type of instance", beanTest.getEntityClass().isInstance(instance));
  }

  /**
   * Test find.
   * @throws Exception the exception
   */
  @SuppressWarnings("unchecked")
  @Test
  public void testFind() throws Exception {
    final List<L> result = new ArrayList<L>();
    final long count = service.find(getSession(), result, (List)null);

    Assert.assertNotNull("Result is null", result);
    Assert.assertEquals("Not all data has been found", ITEMS_TO_INSERT, result.size());
    Assert.assertEquals("Not all data has been found", ITEMS_TO_INSERT, count);
  }

  /**
   * Test find by creation date.
   * @throws Exception the exception
   */
  @Test
  public void testFindByCreationDate() throws Exception {
    Thread.sleep(50);

    final DateTime date2 = new DateTime();
    final List<L> result = new ArrayList<L>();

    service.findByCreationDate(getSession(), getStartDate(), date2, result, null);

    Assert.assertNotNull("Result is null", result);
    Assert.assertEquals("Not all data has been found", ITEMS_TO_INSERT, result.size());
  }

  /**
   * Test find by modification date.
   * @throws Exception the exception
   */
  @Test
  public void testFindByModificationDate() throws Exception {
    Thread.sleep(50);

    final List<L> result = new ArrayList<L>();

    service.findByModificationDate(getSession(), getStartDate(), new DateTime(), result, null);

    Assert.assertNotNull("Result is null", result);
    Assert.assertEquals("Not all data has been found", ITEMS_TO_INSERT, result.size());
  }

  /**
   * Test find by primary key.
   * @throws Exception the exception
   */
  @Test
  public void testFindByPrimaryKey() throws Exception {
    final List<T> inserted = getTestData();
    final T data = inserted.get(0);
    final T result = service.findByKey(getSession(), data.getKey());

    Assert.assertNotNull("Result is null", result);
    Assert.assertEquals("Not all data has been found", data.getKey(), result.getKey());
    Assert.assertEquals("Not all data has been found", data, result);
  }

  /**
   * Test find first row.
   * @throws Exception the exception
   */
  @Test
  public void testFindFirstRow() throws Exception {
    final List<IQueryParameter> params = new ArrayList<IQueryParameter>();
    final IQueryParameter param = QueryParameterFactory.getSingleton().borrow();

    try {
      param.setType(IQueryParameter.EParameterType.FIRST_ROW).setValue(Integer.valueOf(2));
      params.add(param);

      final List<L> result = new ArrayList<L>();

      service.find(getSession(), result, params);

      Assert.assertNotNull("Result is null", result);
      Assert.assertEquals("Not all data has been found", ITEMS_TO_INSERT - 2, result.size());
    }
    finally {
      QueryParameterFactory.getSingleton().release(param);
    }
  }

  /**
   * Test find max rows.
   * @throws Exception the exception
   */
  @Test
  public void testFindMaxRows() throws Exception {
    final List<IQueryParameter> params = new ArrayList<IQueryParameter>();
    final IQueryParameter param = QueryParameterFactory.getSingleton().borrow();

    try {
      param.setType(IQueryParameter.EParameterType.MAX_ROWS).setValue(Integer.valueOf(5));
      params.add(param);

      final List<L> result = new ArrayList<L>();

      service.find(getSession(), result, params);

      Assert.assertNotNull("Result is null", result);
      Assert.assertEquals("Not all data has been found", 5, result.size());
    }
    finally {
      QueryParameterFactory.getSingleton().release(param);
    }
  }

  /**
   * Test persist entity.
   * @throws Exception the exception
   */
  @Test
  public void testInsertEntities() throws Exception {
    final List<T> items = create(ITEMS_TO_INSERT);

    service.insert(getSession(), items);

    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT * 2, count());
  }

  /**
   * Test persist entity.
   * @throws Exception the exception
   */
  @Test
  public void testInsertEntity() throws Exception {
    final T item = create(1).get(0);

    service.insert(getSession(), item);

    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT + 1, count());
    Assert.assertNotNull("Data is null", item);
    Assert.assertNotNull("Data is null", item.getKey());
    Assert.assertNotNull("Data is null", item.getCreationDate());
    Assert.assertNotNull("Data is null", item.getModificationDate());
  }

  /**
   * Test remove entities.
   * @throws Exception the exception
   */
  @Test
  public void testDeleteEntities() throws Exception {
    final List<T> items = getTestData();

    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, count());

    service.delete(getSession(), items);

    Assert.assertEquals("Not all data has been removed", 0, count());
  }

  /**
   * Test persist entity.
   * @throws Exception the exception
   */
  @Test
  public void testUpdateConcurrentEntity() throws Exception {
    final PersistenceSession session = getSession();
    final T item = getTestData().get(0);
    final ConcurrentThread thread = new ConcurrentThread(item.getKey());
    Exception exception = null;

    thread.start();
    modify(item);
    thread.join(1000);

    try {
      service.update(session, item);

      session.getTransaction().commit();
    }
    catch (final Exception e) {
      exception = e;
      e.printStackTrace();

      session.getTransaction().rollback();
    }

    Assert.assertNotNull("No exception thrown", exception);
    Assert.assertTrue("Wrong exception thrown", exception instanceof PersistenceException);
  }

  /**
   * Test persist entity.
   * @throws Exception the exception
   */
  @Test
  public void testUpdateWithoutChanges() throws Exception {
    PersistenceSession session = getSession();
    final T inserted = getTestData().get(0);

    session.getTransaction().commit();

    final int hash = inserted.hashCode();
    final DateTime date = inserted.getModificationDate();

    Assert.assertNotNull("Data is null", inserted.getKey());
    Assert.assertNotNull("Data is null", inserted.getCreationDate());
    Assert.assertNotNull("Data is null", inserted.getModificationDate());

    session.getTransaction().begin();

    final T selectedItem = service.findByKey(session, inserted.getKey());

    selectedItem.setArchived(selectedItem.isArchived());
    service.update(session, selectedItem);
    session.getTransaction().commit();

    // System.out.println(inserted);
    // System.out.println(selectedItem);

    Assert.assertEquals("Row has been updated", hash, selectedItem.hashCode());
    Assert.assertTrue("Row has been updated",
        TimeUtil.getInstance().equals(date, selectedItem.getModificationDate()));

  }

  /**
   * Test persist entities.
   * @throws Exception the exception
   */
  @Test
  public void testUpdateEntities() throws Exception {
    final PersistenceSession session = getSession();
    final List<T> items = getTestData();

    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, count());

    for (final T item : items) {
      modify(item);
    }

    service.update(session, items);

    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, count());

    for (final T item : items) {
      Assert.assertEquals("Data not updated", item, service.findByKey(session, item.getKey()));
    }
  }

  /**
   * Test persist entity.
   * @throws Exception the exception
   */
  @Test
  public void testUpdateEntity() throws Exception {
    final PersistenceSession session = getSession();
    final long count = count();
    final T item = getTestData().get(0);
    final DateTime creationDate = item.getCreationDate();

    Assert.assertNotNull("Data is null", item.getKey());
    Assert.assertNotNull("Data is null", creationDate);
    Assert.assertNotNull("Data is null", item.getModificationDate());

    getSession().getTransaction().commit();
    getSession().getTransaction().begin();

    T selectedItem = service.findByKey(session, item.getKey());

    ((IDataObject)item).setCreationDate(null); // non updatable column
    modify(item);
    service.update(session, item);
    ((IDataObject)item).setCreationDate(creationDate);

    Assert.assertEquals("Not all data has been inserted", count, count());

    selectedItem = service.findByKey(session, item.getKey());

    Assert.assertEquals("Data not updated", item, selectedItem);
    Assert.assertEquals("Data is wrong", creationDate, selectedItem.getCreationDate());
  }
}
